0%

Problem - C - Codeforces

这题有2个解法,然而我赛时一个没写出来,这不是废物是什么?

解法一

对于任意区间 [L,R]

换算一下,得到:

注意到

求$\Delta$的最大值即可

怎么没写出来呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <bits/stdc++.h>
using namespace std;

const int N = 1e9 + 5;

void solve(){

int n;
cin >> n;
vector<int> a(n + 1), pre(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}

int mx1 = 0, mx2 = 0;
for (int i = 1; i <= n; ++i) {
int x = pre[i - 1] - i * (i - 1);
mx1 = max(x, mx1);
x = i * (i + 1) - pre[i];
mx2 = max(x, mx2);
}

cout << pre[n] + mx1 + mx2 << endl;
}

int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int _ = 1;
cin >> _;
while (_--)
solve();
return 0;
}

这是错的,因为L要小于R,而我取了全局最大:

1
2
3
4
5
6
7
int mx1 = 0, mx2 = 0;
for (int i = 1; i <= n; ++i) {
int x = pre[i - 1] - i * (i - 1);
mx1 = max(x, mx1);
x = i * (i + 1) - pre[i];
mx2 = max(x, mx2);
}

正解是:

1
2
3
4
5
6
7
int mx = 0, ans = 0;
for (int i = 1; i <= n; ++i) {
int x = pre[i - 1] - i * (i - 1);
mx = max(x, mx);
x = i * (i + 1) - pre[i] + mx;
ans = max(x, ans);
}

这样可以保证L<=R

妙啊

AC码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <bits/stdc++.h>
using namespace std;

const int N = 1e9 + 5;

void solve(){

int n;
cin >> n;
vector<int> a(n + 1), pre(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}

int mx = 0, ans = 0;
for (int i = 1; i <= n; ++i) {
int x = pre[i - 1] - i * (i - 1);
mx = max(x, mx);
x = i * (i + 1) - pre[i] + mx;
ans = max(x, ans);
}

cout << pre[n] + ans << endl;
}

int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int _ = 1;
cin >> _;
while (_--)
solve();
return 0;
}

解法二

对于任意区间 [L,R]

原区间和为:

替换后的区间和:

神奇的来了,注意到

这是什么?

这是等差序列的求和公式!:

$$S_n = \dfrac{{项数}\times (\text{首项} + \text{末项})}{2}$$

所以:

所以$T$也可以写成:

$$\displaystyle{2 \times \sum_{i=L}^{R}} i = \displaystyle{ \sum_{i=L}^{R}} 2i$$

所以$\Delta$可以写成:

$$\displaystyle{\Delta = T - \displaystyle\sum_{i=L}^{R} a_i = \sum_{i=L}^{R} {(2i - a_i)}}$$

于是问题简化成:

寻找区间,使得 $\displaystyle{\Delta = \sum_{i=L}^{R} {(2i - a_i)}}$ 最大

这已经是标准最大子段和问题了

AC码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<bits/stdc++.h>
using namespace std;

const int N=1e9+5;

void solve(){

int n; cin >> n;
vector<int> a(n + 1), b(n + 1);
long long sum = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
sum += a[i];
b[i] = 2 * i - a[i];
}

long long ans = 0, tot = 0;
for (int i = 1; i <= n; ++i) {
tot = max(0LL, tot + b[i]);
ans = max(tot, ans);
}

cout << ans + sum << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
cin>>_;
while(_--)
solve();
return 0;

}

二维dp的回溯以及四维dp

P1005 NOIP 2007 提高组] 矩阵取数游戏 - 洛谷

这题我一开始用两次二维dp解,每次取最优路(类似贪心)。可是我不会dp回溯,于是有了以下题解:

二维dp的回溯

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void back(int x, int y) {

if (x == 1 && y == 1) {
return;
}

if (x > 1 && dp[x - 1][y] + a[x][y] == dp[x][y]) {
back(x - 1, y);
}
else if (y > 1 && dp[x][y - 1] + a[x][y] == dp[x][y]) {
back(x, y - 1);
}
}

很好懂吧,因为每次dp取得都是到当前格的最大值,因此路径一定存在,一步步减回去就行。

完整码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

const int N=1e9+5;

int dp1[11][11], dp2[11][11];
int a[11][11];

void back(int x, int y) {

if (x == 1 && y == 1) {
a[x][y] = 0;
return;
}

if (x > 1 && dp1[x - 1][y] + a[x][y] == dp1[x][y]) {
a[x][y] = 0;
back(x - 1, y);
}
else if (y > 1 && dp1[x][y - 1] + a[x][y] == dp1[x][y]) {
a[x][y] = 0;
back(x, y - 1);
}
}

void solve(){

int n; cin >> n;
while(1) {
int x, y, z;
cin >> x >> y >> z;
if (x == 0 && y == 0 && z == 0) {
break;
}
else {
a[x][y] = z;
}
}

int sum = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
dp1[i][j] = max(dp1[i - 1][j], dp1[i][j - 1]) + a[i][j];
}
}

sum += dp1[n][n];
back(n, n);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
dp2[i][j] = max(dp2[i - 1][j], dp2[i][j - 1]) + a[i][j];
}
}

sum += dp2[n][n];
cout << sum << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

注意两点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void back(int x, int y) {

// a[x][y] = 0; 不要写在这里,a[x][y]后面会用到,这里不要先改,到if后面再改 !!!

if (x == 1 && y == 1) {
a[x][y] = 0;
return;
}

if (x > 1 && dp1[x - 1][y] + a[x][y] == dp1[x][y]) { // x > 1 不要越界
a[x][y] = 0;
back(x - 1, y);
}
else if (y > 1 && dp1[x][y - 1] + a[x][y] == dp1[x][y]) { // 同理, y > 1 不要越界
a[x][y] = 0;
back(x, y - 1);
}
}

果然pa了,因为当前最优不代表全局最优,贪心思路不可行,反例:

7
1 2 2
1 3 3
2 2 3
3 2 3
5 4 4
6 4 4
7 2 2
7 4 4
0 0 0

1
2
3
4
5
6
7
0 2 3 0 0 0 0
0 3 0 0 0 0 0
0 3 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 4 0 0 0
0 0 0 4 0 0 0
0 2 0 4 0 0 0

答案是25,然而我的解法第一次是:

1
2
3
4
5
6
7
0 0 3 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 2 0 0 0 0 0

得20,第二次得23。

实际上是第一次:

1
2
3
4
5
6
7
0 0 3 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 4 0 0 0
0 0 0 4 0 0 0
0 0 0 0 0 0 0

第二次全吞完,得25。

所以得用四维dp做。

四维dp

直接上代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

int dp[11][11][11][11]; // dp[i][j][p][q]表示第一遍走到点(i, j),第二遍走到点(p, q)的最优解
int a[11][11];

void solve(){

int n; cin >> n;
while(1) {
int x, y, z;
cin >> x >> y >> z;
if (x == 0 && y == 0 && z == 0) {
break;
}
else {
a[x][y] = z;
}
}

for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
for (int p = 1; p <= n; ++p) {
for (int q = 1; q <= n; ++q) {
dp[i][j][p][q] = max(max(dp[i-1][j][p-1][q], dp[i-1][j][p][q-1]), max(dp[i][j-1][p-1][q], dp[i][j-1][p][q-1])) + a[i][j] + a[p][q];
if (i == p && j == q) dp[i][j][p][q] -= a[i][j];
}
}
}
}

cout << dp[n][n][n][n] << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

简单二进制状压

Problem - C - Codeforces

打表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

const int N=1e9+5;

void solve(){

ll n; cin >> n;
vector<int> a(n + 1);
int ans = 0;
for (int i = 1; i <= n; ++i) {
a[i] = i;
ans += i * (n - i + 1);
}
int cnt = 0;
do {
int sum = 0;
for (int i = 1; i <= n; ++i) {
int mn = 0x3f3f3f3f;
for (int j = i; j <= n; ++j) {
mn = min(mn, a[j]);
sum += mn;
}
}

if (sum == ans){
++cnt;
cout << "\033[0m" << cnt << ": ";
for (int i = 1; i <= n; ++i) {
if (a[i] == 3) cout << "\033[32m" << a[i] << " ";
else cout << "\033[31m" << a[i] << " ";
}
cout << endl;
}
}while(next_permutation(a.begin() + 1, a.end()));

}

int main(){

//ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
system("pause");
return 0;

}

输入5为例:

1763829785661

1763829837661

不难发现:一半1,一半1里有一半2,一半2里又有一半3…

超过半数往后站,不过半往前站

因此有解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

void solve(){

ll n, k; cin >> n >> k;
ll m = n;
vector<int> a(n + 1);

ll sum = 1LL;
for (int i = 1; i < n; ++i) {
sum <<= 1LL;
if (sum >= 1e12) {
sum = 1e12;
break;
}
}

if (k > sum) {
cout << -1 << endl;
return;
}

int l = 1, r = n;
ll ls = 0LL, rs = sum;
for (int i = 1; i <= n; ++i) {
ll mid = ((ls + rs) >> 1LL);
if (k <= mid) {
a[l] = i; //不过半往前站
++l;
rs = mid; //集中前半段
}
else {
a[r] = i; //超过半数往后站
--r;
ls = mid; //集中后半段
}
}

for (int i = 1; i <= n; ++i) {
cout << a[i] << " ";
}
cout << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
cin>>_;
while(_--)
solve();
return 0;

}

但是,这么解是错的,让我们听听AI怎么说。

对于

1
2
3
4
5
6
7
输入:
1
60 2

我输出的是: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 40 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 39

正确答案是: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 60 59

为什么错了?

你把 sum = 2^(n-1) 人为截断成 1e12
n=60 时真实的 $2^{n-1}=2^{59}≈5.76e17$,而你把它设成 1e12。以后你用 [ls,rs) 以二分区间把 k 映射成“左右选择”的逻辑 依赖 rs-ls 恰好是 2^(remaining) 的幂次。截断后这个不再成立,映射就被彻底破坏了 —— 因此决策在第 ~39 次才从“放左”变成“放右”,产生你看到的 1..38 然后一大段乱序的结果。

什么意思,原本sum很长很长,可以截很多个二分,但因为原本sum太长太长了,long long存不下,我把它截成 1e12(k的最大值),导致截的二分数变少了,mid很快扰乱了如下代码的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
for (int i = 1; i <= n; ++i) {
ll mid = ((ls + rs) >> 1LL);
if (k <= mid) {
a[l] = i; //不过半往前站
++l;
rs = mid; //集中前半段
}
else {
a[r] = i; //超过半数往后站
--r;
ls = mid; //集中后半段
}
}

不妨想一下:

二进制状压

我们这样打表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

const int N=1e9+5;

void solve(){

ll n; cin >> n;
vector<int> a(n + 1);
int ans = 0;
for (int i = 1; i <= n; ++i) {
a[i] = i;
ans += i * (n - i + 1);
}
int cnt = 0;
do {
int sum = 0;
for (int i = 1; i <= n; ++i) {
int mn = 0x3f3f3f3f;
for (int j = i; j <= n; ++j) {
mn = min(mn, a[j]);
sum += mn;
}
}

if (sum == ans){
++cnt;
cout << "\033[0m" << bitset<4>(cnt - 1)<< ": "; //以5为例(0 ~ 15) 16种情况
for (int i = 1; i <= n; ++i) {
if (a[i] == 1) cout << "\033[32m" << a[i] << " ";
else cout << "\033[31m" << a[i] << " ";
}
cout << endl;
}
}while(next_permutation(a.begin() + 1, a.end()));

}

int main(){

//ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
system("pause");
return 0;

}

img

我们可以想到,我们之前的步骤中,每一次二分相当于消去最高位,观察下一位,如果该位是0,往前站,该位是1,往后站,最后剩下的一位,填最后一位(n)。

假设n = 5, 当前位置为k = 8,由于从0计数,所以减1,得7,二进制为0111,于是,我们有:

1
2
3
4
5
6
0111: 1
111: 1 2
11: 1 3 2
1: 1 4 3 2

最后:1 5 4 3 2

完美!

我们可以反过来,从n到1填,毕竟从低位到高位遍历数位要容易很多,不断除2就行。填数就用双端队列一个个往里挤,很方便。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

void solve(){

ll n, k; cin >> n >> k;
ll m = n;
vector<int> a(n + 1);

ll sum = 1LL;
for (int i = 1; i < n; ++i) {
sum <<= 1LL;
if (sum >= 1e12) {
sum = 1e12;
break;
}
}

if (k > sum) {
cout << -1 << endl;
return;
}

ll b = k - 1; //从0计数,减1
deque<ll> dq;
dq.push_back(n);
for (ll x = n - 1; x >= 1; --x) {
if (b & 1LL) dq.push_back(x);
else dq.push_front(x);
b >>= 1;
}

while(!dq.empty()) {
cout << dq.front() << " ";
dq.pop_front();
}
cout << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
cin>>_;
while(_--)
solve();
return 0;

}

AC了!

那你可能会想:前面说那么多,不还是观察出来的吗?

是的。

从逐次减法到高效除法

题目描述:

给定初始值 N,以及 M 种操作,每种操作 (a, b),表示每次操作必须满足 N >= a,然后 N 会减少 b。要求计算最多可以执行多少次操作,并更新 N


思路与推导:

  1. 操作描述
    每个操作 (a, b),表示:

    • 只有当 N >= a 时,才能进行操作。
    • 每次操作会将 N 减去 b,直到无法继续执行为止。
  2. 最大操作次数的推导
    假设对于某个操作 ab,我们希望计算最多可以执行多少次该操作。

    由于每次操作将 N 减少 b,为了保证 N 始终大于等于 a,我们可以得到以下条件:

    其中 t 为最大执行次数。由此可以得到:

    这意味着最多可以进行 $t = \left\lfloor \frac{N - a}{b} \right\rfloor$ 次操作。

    为什么要加 1

    • 因为N在减了t次之后还是大于或等于a的还可以再进行一次操作,这次减了b了之后,N小于a,不会再进行下一次(a,b)操作了

    所以,最终的最大操作次数是:

  3. 更新 N
    每次操作执行 t 次后,N 会减少 t * b。更新后的 N 为:

    继续进行下一个操作,直到遍历所有操作。


示例:

输入:

1
2
3
100 2
20 3
50 5

输出:

1
27

解释:

  1. 第一个操作a = 20, b = 3,计算 (100 - 20) / 3 + 1 = 27,执行 27 次,更新 N = 100 - 27 * 3 = 19
  2. 第二个操作a = 50, b = 5,由于 N = 19 小于 50,跳过该操作。
  3. 最终输出 cnt = 27

总结:

通过优化循环,使用数学公式一次性计算每个操作的最大执行次数,避免了逐次减法,从而提高了程序效率。核心考察了通过数学推导解决动态问题和优化计算时间的能力。

https://ac.nowcoder.com/acm/contest/120454/F

属于典型的“是否存在一个 X ∈ R 可以满足所有约束”的问题

浮点二分

二分解法: 二分答案$r^2$,然后将区间$x-\sqrt{r^2-y^2}$到$x+\sqrt{r^2-y^2}$ 区间覆盖判断,若所有区间存在交集,则返回true

先看错解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<bits/stdc++.h>
using namespace std;
struct node {
double x, y;
};

void solve(){

int n; cin >> n;
vector<node> a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i].x >> a[i].y;
}
auto check = [&](double R2) -> bool {
double L = -1e9, R = 1e9;
for (int i = 1; i <= n; ++i) {
double X = R2 - a[i].y * a[i].y;
double l = a[i].x - sqrt(X);
double r = a[i].x + sqrt(X);
L = max(L, l);
R = min(R, r);
}
return L <= R;
};
double l = -1e9, r = 1e9;
double ans;
while (l <= r) {
double mid = l + (r - l) / 2.0;
if (check (mid)) {
ans = mid;
l = mid + 1;
}
else {
r = mid - 1;
}
}
cout << sqrt(ans) << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

错误点:

  • 在二分一个 double,却使用了整数二分写法

    1
    2
    3
    4
    5
    while (l <= r) {
    mid = l + (r - l) / 2;
    if (check(mid)) l = mid + 1;
    else r = mid - 1;
    }

    使用这种比较完全不收敛,
    而且 mid + 1 会跳过正确答案,导致永远不精确。

  • check(R2) 没有边界判断

    如果 $r^2$ < $y^2$,$x^2$为负,会 sqrt(负数) → nan → WA/RE

  • 二分区间不应该是 [-1e9, 1e9]

    [-1e9, 1e9] 有一半是无效区间
    因为我们二分答案 $r^2$ ,而半径平方不能为负

正确写法:浮点二分(模板)

1
2
3
4
5
6
7
double l = 0, r = 2e8;   // 1e4^2 + 1e4^2
for (int i = 0; i < 60; i++) {
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}

不能用 while(l <= r)!!

最终无 bug 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<bits/stdc++.h>
using namespace std;
struct node {
double x, y;
};

void solve(){

int n; cin >> n;
vector<node> a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i].x >> a[i].y;
}
auto check = [&](double R2) -> bool {
double L = -1e9, R = 1e9;
for (int i = 1; i <= n; ++i) {
double X = R2 - a[i].y * a[i].y;

if (X < 0) return 0; //若圆与x轴无交点,即X < 0,直接返回false,否则sqrt(X)会nan

double l = a[i].x - sqrt(X);
double r = a[i].x + sqrt(X);
L = max(L, l);
R = min(R, r);
}
return L <= R;
};

double l = 0, r = 1e9;
for (int i = 1; i <= 60; ++i) {
double mid = l + (r - l) / 2;
if (check(mid)) {
r = mid; //这里应该以r为答案,之前连这里都错了!
}
else {
l = mid;
}
}

cout << setprecision(6) << sqrt(r) << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

三分

三分法是一种用于在“单峰/单谷函数f(x)”上寻找最优值(最大或最小)的算法。

三分法思想:

把区间 [l, r] 分成三段: l —— m1 —— m2 —— r

其中: m1 = l + (r - l) / 3 (左三等分点)
m2 = r - (r - l) / 3(右三等分点)

然后比较: f(m1) 和 f(m2)

如果 f(m1) < f(m2)
→ 最高点在区间 (m1, r)
→ 扔掉左边部分 l = m1

如果 f(m1) > f(m2)
→ 最高点在区间 (l, m2)
→ 扔掉右边部分 r = m2

三分法代码模板(整数区间)

1
2
3
4
5
6
7
8
9
10
11
12
int ternary(int l, int r) {
while (r - l >= 3) {
int m1 = l + (r - l) / 3;
int m2 = r - (r - l) / 3;

if (f(m1) < f(m2))
l = m1;
else
r = m2;
}
}

三分法代码模板(实数区间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
double ternary(double l, double r) {
const double eps = 1e-12;

while (r - l > eps) {
double m1 = l + (r - l) / 3;
double m2 = r - (r - l) / 3;

if (f(m1) < f(m2)) {
l = m1;
} else {
r = m2;
}
}
}

算了,直接看题吧

我们要在 x 轴上选一个点 X,使得到所有点 ($x_i$, $y_i$) 的 最大距离最小

$f(x) = \max\limits_{1 \le i \le n}\sqrt{(X - x_i)^2 + y_i^2}$

自己感受一下:$X$从x轴左边到x轴右边,$\max\limits_{1 \le i \le n}\sqrt{(X - x_i)^2 + y_i^2}$ 肯定先变小后变大

所以这个函数 是凹的(单峰),我们要搜$f(x)$ 的最小值(对应$x$为X),因此可用 三分法 搜索 X。

代码(AC 版本,C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<bits/stdc++.h>
using namespace std;

struct node {
int x, y;
};

void solve(){

int n; cin >> n;
vector<node> a(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i].x >> a[i].y;
}

auto f = [&](double x) -> double {
double mx = 0;
for (int i = 1; i <= n; ++i) {
double dx = a[i].x - x, dy = a[i].y;
mx = max(mx, sqrt(dx * dx + dy * dy));
}
return mx;
};

double l = -10000, r = 10000;
for (int i = 1; i <= 60; ++i) {
double m1 = l + (r - l) / 3;
double m2 = r - (r - l) / 3;
if (f(m1) > f(m2)) {
l = m1;
}
else {
r = m2;
}
}

cout << setprecision(6) << f((l + r) / 2.0) << endl;

}

int main(){

ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int _=1;
while(_--)
solve();
return 0;

}

收工!

逆推概率笔记

一、背景

  • 题目目标:计算多个区间随机出现时,每个格子恰好被一个区间覆盖的总概率。
  • 简化版本:去掉取模、逆元,用浮点数 p/q 表示区间出现概率。

二、核心流程

  1. 初始化 “都不出现” 的概率乘积ans

    • 每条区间不出现概率 = (q - p)/q
    • ans = ∏ (q - p)/q
  2. 动态规划 (DP)

    • 状态:dp[i] 表示从格子 1 到 i,在“都不出现”的前提下,恰好被覆盖一次的概率。
    • 初始:dp[0] = 1dp[1..m] = 0
  3. 区间排序与转移

    • 先按 lr 排序区间。
    • 对第 i 条区间 [l, r] 做:

      1
      dp[r] += dp[l - 1] * (p / (q - p));
  4. 结果计算

    • 最终答案 = ans * dp[m]

三、关键疑问与解答

问题1:为什么要除以 (q - p)

  • 直觉误区:在“不出现”情况下,出现概率似乎为 0。
  • 条件概率:反向推导就是在“不出现”的条件下,推导“出现”的修正因子。
  • 公式推导
    $P(出现|不出现) = \frac{P(出现)}{P(不出现)} = \frac{(p/q)}{((q-p)/q)} = \frac{p}{q - p}.$

问题2:什么是“反向推导”?

  • 指在“都不出现”的概率空间中,逆向演算某条区间突然出现对最终结果的影响。
  • 作用:给 dp 转移添加正确的概率修正因子。

四、小例子演示

  • m = 3, 区间 A:[1,2], p/q=1/2; B:[3,3], p/q=1/3
  • ans = (1/2)*(2/3) = 1/3
  • DP 转移:

    1. A: dp[2] += dp[0] * (1/(2-1)) = 1dp[2] = 1
    2. B: dp[3] += dp[2] * (1/(3-1)) = 1/2dp[3] = 0.5
  • 最终:ans * dp[3] = 1/3 * 1/2 = 1/6

五、简化版代码(去模版、取模、快速幂)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Node {
double l, r, p, q;
};

bool cmp(Node a, Node b) {
if (a.l == b.l) return a.r < b.r;
return a.l < b.l;
}

int main() {
int n, m;
cin >> n >> m;
vector<Node> seg(n);
double ans = 1.0;

for (int i = 0; i < n; ++i) {
cin >> seg[i].l >> seg[i].r >> seg[i].p >> seg[i].q;
ans *= (seg[i].q - seg[i].p) / seg[i].q;
}

sort(seg.begin(), seg.end(), cmp);
vector<double> dp(m + 5, 0);
dp[0] = 1.0;

for (int i = 0; i < n; ++i) {
dp[(int)seg[i].r] += dp[(int)seg[i].l - 1] * seg[i].p / (seg[i].q - seg[i].p);
}

cout << ans * dp[m] << endl;
return 0;
}

备注:以上笔记可帮助队友快速理解“为什么除以 (q-p)”及反向推导思路。

六、示意图解

1
2
3
4
5
6
7
8
9
10
11
12
起点 dp[0] = 1.0
|
+-- 区间 A [1,2]
p/q = 1/2,不出现概率 (q-p)/q=1/2
修正因子 p/(q-p)=1/(2-1)=1
=> dp[2] += dp[0] * 1 = 1.0
|
+-- 区间 B [3,3]
p/q = 1/3,不出现概率 (q-p)/q=2/3
修正因子 p/(q-p)=1/(3-1)=1/2
=> dp[3] += dp[2] * 1/2 = 0.5
最终答案 = ans * dp[3] = (1/2*2/3) * 0.5 = 1/6

欧拉函数 (Euler’s Totient Function) 详解

欧拉函数 φ(n) 是数论中一个非常重要的函数,它表示小于或等于 n 的正整数中与 n 互质的数的个数。

定义

对于正整数 n,欧拉函数 φ(n) 定义为:

1
φ(n) = 小于或等于 n 的正整数中与 n 互质的数的个数

其中”互质”意味着两个数的最大公约数为1。

计算方法

1. 对于质数 p

如果 p 是质数,那么:

1
φ(p) = p - 1

因为1,2,…,p-1都与p互质。

2. 对于质数的幂 pᵏ

1
φ(pᵏ) = pᵏ - pᵏ⁻¹ = pᵏ(1 - 1/p)

因为在1到pᵏ之间,只有p的倍数不与pᵏ互质,这样的数有pᵏ⁻¹个。

3. 对于任意正整数 n

利用中国剩余定理和积性函数性质,如果n的标准质因数分解为:

1
n = p₁ᵏ¹ × p₂ᵏ² × ... × pₘᵏᵐ

那么:
1
φ(n) = n × (1 - 1/p₁) × (1 - 1/p₂) × ... × (1 - 1/pₘ)

示例计算

  1. φ(10):

    1
    2
    10 = 2 × 5
    φ(10) = 10 × (1 - 1/2) × (1 - 1/5) = 10 × 1/2 × 4/5 = 4

    实际与10互质的数有1,3,7,9,共4个。

  2. φ(12):

    1
    2
    12 = 2² × 3
    φ(12) = 12 × (1 - 1/2) × (1 - 1/3) = 12 × 1/2 × 2/3 = 4

    实际与12互质的数有1,5,7,11,共4个。

在代码中的应用

在你提供的代码中,计算了φ(210):

1
2
3
4
210 = 2 × 3 × 5 × 7
φ(210) = 210 × (1 - 1/2) × (1 - 1/3) × (1 - 1/5) × (1 - 1/7)
= 210 × 1/2 × 2/3 × 4/5 × 6/7
= 48

这意味着在每210个连续整数中,有48个数不被2,3,5,7整除(即与210互质)。

性质

  1. 积性函数:如果m和n互质,则φ(mn) = φ(m)φ(n)
  2. 欧拉定理:如果a和n互质,则a^φ(n) ≡ 1 (mod n)
  3. 费马小定理:当n是质数p时的特例,a^(p-1) ≡ 1 (mod p)

欧拉函数在密码学(如RSA算法)、数论和组合数学中都有广泛应用。

位运算小技巧总结

1. 奇偶性快速判断与反转

1
2
3
4
5
// 判断奇偶性
bool is_odd = x & 1; // 真为奇数,假为偶数

// 0/1交替
n ^= 1;

2. 二进制位操作

1
2
3
4
5
6
7
8
// (1) 取第k位(从0开始)
int bit = (n >> k) & 1;

// (2) 设置第k位为1
n |= (1 << k);

// (3) 设置第k位为0
n &= ~(1 << k);

3. 位遍历(判断当前位是 1 还是 0 )

1
2
3
4
5
6
7
8
9
while(num) {           // 当num不为0时循环
if(num & 1) {
// 当前位为1的处理逻辑
}
else{
// 当前位为0的处理逻辑
}
num >>= 1; // 右移一位(相当于除以2)
}

4. 快速计算二进制1的个数(__builtin_popcount)

1
2
3
4
5
6
7
int cnt = __builtin_popcount(x); // GCC内置函数
// 等价于:
int cnt = 0;
while(x) {
x &= x - 1; // 清除最低位的1
++cnt;
}

5. 保留最低位的1(lowbit)

1
2
int lowbit = x & -x;  // 树状数组核心操作
// 例如:x=6(110) → 2(10)

6. 判断/寻找0位

1
2
3
4
5
6
7
8
int t = 1ll;
for (int i = 0; i <= 63;++i){
if(!(n&t)){ //核心判断条件
//n的第i位为0
}

t <<= 1;
}

7. 判断是否为2的幂

1
2
bool is_power_of_two = (x & (x - 1)) == 0;
// 注意特判x=0时为false

8. 位运算优先级口诀

1
~ > 算术运算 > << >> > & > ^ > |

注意事项

  1. 位运算优先级容易出错,建议多用括号
  2. 右移时注意符号位(无符号用>>,有符号用>>>
  3. 大数运算记得加1LL防止溢出

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment